Skip to content

Add Internal v2 API Access Token Generation for Users#3597

Open
aaronskiba wants to merge 11 commits intoapi_v2_dmponlinefrom
aaron/feature/v2-api-token-for-internal-users
Open

Add Internal v2 API Access Token Generation for Users#3597
aaronskiba wants to merge 11 commits intoapi_v2_dmponlinefrom
aaron/feature/v2-api-token-for-internal-users

Conversation

@aaronskiba
Copy link
Contributor

@aaronskiba aaronskiba commented Feb 9, 2026

Changes proposed in this PR:

Summary

This PR introduces support for issuing internal, user-scoped v2 API access tokens, managed entirely within the application (first-party tokens) for internal users.

Key Changes

Create internal Doorkeeper app via rake task

  • Executed via bundle exec rake doorkeeper:ensure_internal_app
  • Creates a first-party Doorkeeper client for issuing internal v2 API tokens
  • Ensures the internal application exists in all environments before token service is used

Create Api::V2::InternalUserAccessTokenService

  • This service manages user-scoped v2 API access tokens for internal app users.
    • Tokens are equivalent to first-party Personal Access Tokens (PATs) and are issued directly to authenticated users, bypassing the full OAuth 2.0 authorization_code flow.
    • Supports token creation, rotation, and revocation.
    • Uses Doorkeeper::AccessToken records for consistent scoping, expiry, and revocation handling.
    • Designed strictly for internal usage; third-party OAuth clients are not supported.

Add "POST /api/v2/internal_user_access_token" action & route

  • Adds Api::V2::InternalUserAccessTokensController#create with Pundit authorization and routing. Also reuses the existing users/refresh_token.js.erb response to update the UI via JS.

  • @success is read by app/views/users/refresh_token.js.erb (similar approach as UsersController#refresh_token)

Add API v2 section to /users/edit#api-details

  • This change updates app/views/devise/registrations/_api_token.html.erb to include support for the v2 API access token. Existing v0/v1 token support is retained.
    • Introduce V2 token lookup via Api::V2::InternalUserAccessTokenService
    • Display a dedicated V2 API access token section with its own regeneration action

Expose API Access tab to all users / restrict legacy token rendering

  • The API Access tab is now visible to all users to support the new v2 API token,
    which is accessible to everyone.

Add test coverage for internal v2 token generation

  • Add request specs for InternalUserAccessTokensController

    • Include both authenticated & unauthenticated user scenarios
    • Include both present & absent internal OAuth app scenarios
  • Add service specs for InternalUserAccessTokenService

    • Test token retrieval, rotation, and OAuth app presence
    • Verify old token revocation when rotating
  • Add view specs for API token partials

    • Test legacy partial rendering based on user.can_use_api?
    • Test OAuth application availability scenarios

@github-actions
Copy link

github-actions bot commented Feb 9, 2026

</tr>
1 Error
🚫

Please include a CHANGELOG entry.

You can find it at [CHANGELOG.md](https://github.com/DMPRoadmap/roadmap/blob/main/CHANGELOG.md).
1 Warning
⚠️ There are code changes, but no corresponding tests. Please include tests if this PR introduces any modifications in behavior. \n
Ignore this warning if the PR ONLY contains translation.io synced updates.

Generated by 🚫 Danger

authorize current_user, :internal_user_v2_access_token?
@token = Api::V2::InternalUserAccessTokenService.rotate!(current_user)
respond_to do |format|
format.js { render 'users/refresh_token' }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible for this endpoint to be hit without JS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's technically possible via a direct POST (but that would fail without proper authentication).

Our UI only initiates this action through JS. This is consistent with the legacy token refresh pattern, where JS rendering is implicit. To reuse refresh_token.js.erb, we need to explicitly add format.js { render 'users/refresh_token' }.

Doorkeeper::AccessToken.find_by(user_token_filter(user))
end

def rotate!(user)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible for this to fail in the creation stage? If so, I'm just wondering if we have ample error handling for that scenario.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will at least add some handling in case the internal OAuth application is not found.

@momo3404
Copy link
Collaborator

There's two documentation related Rubocop errors

app/controllers/api/v2/internal_user_access_tokens_controller.rb:5:5: C: Style/Documentation: Missing top-level documentation comment for class Api::V2::InternalUserAccessTokensController.
app/services/api/v2/internal_user_access_token_service.rb:23:5: C: Style/Documentation: Missing top-level documentation comment for class Api::V2::InternalUserAccessTokenService.

@aaronskiba aaronskiba force-pushed the aaron/feature/v2-api-token-for-internal-users branch 3 times, most recently from a5c24c6 to 23a2230 Compare February 13, 2026 21:26
- Creates a first-party Doorkeeper client for issuing internal v2 API tokens
- Sets redirect_uri to OOB, scopes to 'read', and marks it as confidential
- Ensures the internal application exists in all environments before token service is used
@aaronskiba aaronskiba force-pushed the aaron/feature/v2-api-token-for-internal-users branch 2 times, most recently from ad94297 to 5c24cdf Compare February 17, 2026 17:03
This service manages user-scoped v2 API access tokens for internal app users.

- Tokens are equivalent to first-party Personal Access Tokens (PATs) and are issued directly to authenticated users, bypassing the full OAuth 2.0 authorization_code flow.
- Supports token creation, rotation, and revocation.
- Uses Doorkeeper::AccessToken records for consistent scoping, expiry, and revocation handling.
- Designed strictly for internal usage; third-party OAuth clients are not supported.
Adds `Api::V2::InternalUserAccessTokensController#create` with Pundit authorization and routing. Also reuses the existing `users/refresh_token.js.erb` response to update the UI via JS.

`@success` is read by `app/views/users/refresh_token.js.erb` (similar approach as `UsersController#refresh_token`)
This change updates `app/views/devise/registrations/_api_token.html.erb` to include support for the v2 API access token. Existing v0/v1 token support is retained.
- Introduce V2 token lookup via `Api::V2::InternalUserAccessTokenService`
- Display a dedicated V2 API access token section with its own
  regeneration action
This change breaks refactors `_api_token.html.erb` into additional separate partials:
1) app/views/devise/registrations/_legacy_api_token.html.erb
2) app/views/devise/registrations/_v2_api_token.html.erb

In addition to the refactor, the following changes have been made:
- `<div id="api-token"` has been renamed to `<div id="legacy-api-token"`
- A `<div id="api-tokens">` wrapper has been added in app/views/devise/registrations/_api_token.html.erb.
  - `app/views/users/refresh_token.js.erb` now references the '#api-tokens' wrapper.
The API Access tab is now visible to all users to support the new v2 API token,
which is accessible to everyone.

The existing v0/v1 legacy token remains restricted and continues to use the
previous authorization and rendering logic within the tab.
Styling changes can be viewed at /users/edit#api-details
`InternalUserAccessTokenService`: add `application!` (lookup + raise) and `application_present?` (safe check with logging)

`_v2_api_token.html.erb`: gate token UI on `application_present?` and show a warning when missing.
Add request specs for InternalUserAccessTokensController
- Include both authenticated & unauthenticated user scenarios
- Include both present & absent internal OAuth app scenarios

Add service specs for InternalUserAccessTokenService
- Test token retrieval, rotation, and OAuth app presence
- Verify old token revocation when rotating

Add view specs for API token partials
- Test legacy partial rendering based on `user.can_use_api?`
- Test OAuth application availability scenarios
Add `defaults: { format: :js }` to the internal_user_access_token route, allowing callers to omit the explicit format parameter.
@aaronskiba aaronskiba force-pushed the aaron/feature/v2-api-token-for-internal-users branch from 5c24cdf to 6a54ad5 Compare February 17, 2026 17:34
@aaronskiba aaronskiba changed the title Aaron/feature/v2 api token for internal users Add Internal v2 API Access Token Generation for Users Feb 17, 2026
@aaronskiba aaronskiba marked this pull request as ready for review February 17, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments